(function () {
  const errorObj = {};
  let tryCatchTarget;

  const tryCatcher = function tryCatcher() {
    try {
      const target = tryCatchTarget;
      tryCatchTarget = null;
      return target.apply(this, arguments);
    } catch (e) {
      errorObj.e = e;
      return errorObj;
    }
  };
  const tryCatch = function tryCatch(fn) {
    tryCatchTarget = fn;
    return tryCatcher;
  };

  const async = {
    schedule(fn) {
      setTimeout(fn, 0);
    },
    invoke(fn, receiver, arg) {
      this.schedule(() => {
        fn.call(receiver, arg);
      });
    },
    settlePromises(promise) {
      this.schedule(() => {
        promise._settlePromises();
      });
    },
  };

  const INTERNAL = function INTERNAL() {};

  const STATUS = {
    PENDING: Symbol('PENDING'),
    FULFILLED: Symbol('FULFILLED'),
    REJECTED: Symbol('REJECTED'),
  }

  class Promise {
    constructor(executor) {
      this.status = STATUS.PENDING;
      this.handlers = [];
      this._isFollowing = false;
      this._followee = null;
      this._resolveFromExecutor(executor);
    }

    _resolveFromExecutor(executor) {
      // if (executor === INTERNAL) return;
      const r = this._execute(executor, (value) => {
        this._resolveCallback(value);
      }, (reason) => {
        this._rejectCallback(reason);
      });

      if (r !== undefined) {
          this._rejectCallback(r);
      }
    }

    _execute(executor, resolve, reject) {
      try {
        executor(resolve, reject);
      } catch (e) {
        return e;
      }
    }

    _resolveCallback(value) {
      if (this.status !== STATUS.PENDING) return;
      if (!(value instanceof Promise)) return this._fulfill(value);

      const p = value._target();

      if (p.status === STATUS.PENDING) {
        const len = this.handlers.length;
        this.handlers.forEach(({ fulfill, reject, promise }) => {
          p._addCallbacks(fulfill, reject, promise);
        });
        this._isFollowing = true;
        this.handlers.length = 0;
        this._followee = p;
      } else if (p.status === STATUS.FULFILLED) {
        this._fulfill(p._value);
      } else if (p.status === STATUS.REJECTED) {
        this._reject(p._reason);
      }
    }

    _target() {
      let ret = this;
      while (ret._isFollowing) ret = ret._followee;
      return ret;
    }

    _fulfill(value) {
      if (this.status !== STATUS.PENDING) return;
      this.status = STATUS.FULFILLED;
      this._value = value;

      async.settlePromises(this);
    }

    _rejectCallback(reason) {
      this._reject(reason);
    }

    _reject(reason) {
      if (this.status !== STATUS.PENDING) return;
      this.status = STATUS.REJECTED;
      this._reason = reason;

      async.settlePromises(this);
    }

    then(didFulfill, didReject) {
      return this._then(didFulfill, didReject);
    }

    _then(didFulfill, didReject) {
      const promise = new Promise(INTERNAL);
      const target = this._target();
      let handler;
      let value;

      if (target.status !== STATUS.PENDING) {
        if (target.status === STATUS.FULFILLED) {
          handler = didFulfill;
          value = target._value;
        } else if (target.status === STATUS.REJECTED) {
          handler = didReject;
          value = target._reason;
        }

        async.invoke(
          function ({ promise, handler, value }) {
            this._settlePromise(promise, handler, value);
          },
          target,
          {
            handler,
            promise,
            value,
          }
        );
      } else {
        target._addCallbacks(didFulfill, didReject, promise);
      }

      return promise;
    }

    catch(fn) {
      return this.then(undefined, fn);
    }

    _addCallbacks(fulfill, reject, promise) {
      this.handlers.push({
        fulfill,
        reject,
        promise,
      });
    }

    _settlePromises() {
      this.handlers.forEach(({ fulfill, reject, promise }) => {
        if (this.status === STATUS.FULFILLED) {
          this._settlePromise(promise, fulfill, this._value);
        } else {
          this._settlePromise(promise, reject, this._reason);
        }
      });
      this.handlers.length = 0;
    }

    _settlePromise(promise, handler, value) {
      if (typeof handler === 'function') {
          this._settlePromiseFromHandler(handler, value, promise);
      } else {
        if (promise.status === STATUS.FULFILLED) {
          promise._fulfill(value);
        } else {
          promise._reject(value);
        }
      }
    }

    _settlePromiseFromHandler(handler, value, promise) {
      const x = tryCatch(handler).call(null, value);

      if (x === errorObj) {
        promise._reject(x.e);
      } else {
        promise._resolveCallback(x);
      }
    }
  }

  Promise.resolve = function resolve(v) {
    return new Promise((res) => {
      res(v);
    });
  };
  Promise.reject = function reject(v) {
    return new Promise((_, rej) => {
      rej(v);
    });
  };

  window.Promise = Promise;

  class PromiseArray {
    constructor(values, count, isAll) {
      this._ps = values;
      this._count = isAll ? values.length : count;
      this._isAll = isAll;
      this._values = [];
      this._valueCount = 0;
      this._reasons = [];
      this._reasonCount = 0;
      this._promise = new Promise(INTERNAL);
      this._iterate();
    }

    _iterate() {
      let p;
      for (let i = 0; i < this._ps.length; i++) {
        p = this._ps[i];
        p.then(function (index, value) {
          if (this._isAll) {
            this._values[index] = value;
          } else {
            this._values.push(value);
          }
          this._valueCount++;
          this._check();
        }.bind(this, i), function (index, reason) {
          if (this._isAll) {
            this._reasons[index] = reason;
          } else {
            this._reasons.push(reason);
          }
          this._reasonCount++;
          this._check();
        }.bind(this, i));
      }
    }

    _check() {
      if (this._count <= this._valueCount) {
        this._promise._fulfill(this._values);
      } else if (this._ps.length - this._count < this._reasonCount) {
        this._promise._reject(this._reasons);
      }
    }
  }

  Promise.all = function (values) {
    return new PromiseArray(values, undefined, true)._promise;
  };
  Promise.some = function (values, count) {
    return new PromiseArray(values, count, false)._promise;
  };
  Promise.any = function (values) {
    return new PromiseArray(values, 1, false)._promise;
  };
})();
